﻿using System;
using System.Linq;
using System.Xml;
using System.Xml.Linq;

namespace SPContrib.Extensions
{
    /// <summary>
    /// Extensions for the XElement class
    /// </summary>
    public static class XElementExtensions
    {
        /// <summary>
        /// Returns an instance of XmlDocument that represents the XElement
        /// </summary>
        public static XmlDocument AsXmlDocument(this XElement instance)
        {
            return TypeMapper.Resolve<XElement, XmlDocument>(instance);
        }

        /// <summary>
        /// Returns an instance of XmlNode that represents the XElement
        /// </summary>
        public static XmlNode AsXmlNode(this XElement instance)
        {
            return instance.AsXmlDocument() as XmlNode;          
        }

        /// <summary>
        /// Extracts and converts the attribute´s value to another type
        /// </summary>
        /// <typeparam name="T">the type to convert the attribute´s value t</typeparam>
        /// <param name="name">the name of the attribute</param>
        /// <exception cref="System.Exception" />
        public static T Attribute<T>(this XElement instance, XName name)
        {
            return instance.Attribute(name).ValueAs<T>();
        }

        /// <summary>
        /// Extracts and converts the attribute´s value to another type
        /// </summary>
        /// <typeparam name="T">the type to convert the attribute´s value t</typeparam>
        /// <param name="name">the name of the attribute</param>
        /// <param name="valueIfNotExists">the value to return if the attribute is either null or an empty string</param>
        public static T Attribute<T>(this XElement instance, XName name, T valueIfNotExists)
        {
            return instance.Attribute(name).ValueAs<T>(valueIfNotExists);
        }

        /// <summary>
        /// Indicates if an attribute with the supplied name can be located with the XElement´s attributes
        /// </summary>
        /// <param name="name">the name of the attribute</param>
        public static bool HasAttribute(this XElement instance, XName name)
        {
            return instance.Attribute(name).IsNotNull();
        }

        /// <summary>
        /// Removes all the namespace attributes from the XElement and returns a "clean" one
        /// </summary>
        public static XElement RemoveNamespaces(this XElement instance)
        {
            XElement result = new XElement
            (
                instance.Name.LocalName,
                instance.HasElements ? instance.Elements().Select(x => x.RemoveNamespaces()) : (object)instance.Value
            );

            result.ReplaceAttributes
                (
                    from attribute in instance.Attributes()
                    where attribute.IsNamespaceDeclaration == false
                    select attribute
                );

            return result;
        }

        /// <summary>
        /// Extracts and converts the elements´s value to another type
        /// </summary>
        /// <typeparam name="T">the type to convert the elements´s value to</typeparam>
        /// <exception cref="System.Exception" />
        public static T ValueAs<T>(this XElement instance)
        {
            // conditions:
            // no converter function (i.e. if no mapping exists an exception is thrown)
            // no default value to use
            ValidateTypeMappingFor<T>(instance);
            return TypeMapper.Resolve<string, T>(instance.Value);
        }

        /// <summary>
        /// Extracts and converts the elements´s value to another type
        /// </summary>
        /// <typeparam name="T">the type to convert the elements´s value to</typeparam>
        /// <param name="valueIfNotExists">the value to return if the element is either null or an empty string</param>
        public static T ValueAs<T>(this XElement instance, T valueIfNotExists)
        {
            if (instance.IsNullOrEmpty())
            {
                return valueIfNotExists;
            }
            return instance.ValueAs<T>();
        }

        /// <summary>
        /// Extracts and converts the elements´s value to another type
        /// </summary>
        /// <typeparam name="T">the type to convert the elements´s value to</typeparam>
        /// <param name="converter">represents a method that converts from a string to another type</param>
        /// <exception cref="System.Exception" />
        public static T ValueAs<T>(this XElement instance, Func<string, T> converter)
        {
            if (TypeMapper.CannotResolveBetween<string, T>())
            {
                TypeMapper.RegisterMappingBetween<string, T>(converter);
            }
            ValidateTypeMappingFor<T>(instance);
            return TypeMapper.Resolve<string, T>(instance.Value);
        }

        private static bool IsNullOrEmpty(this XElement instance)
        {
            return instance.IsNull() || instance.Value.IsNullOrEmpty();
        }

        private static void ValidateTypeMappingFor<T>(this XElement instance)
        {
            if (instance.IsNull())
            {
                throw "The element does not exists (i.e. the instance of it is null)"
                        .AsException();
            }
            else if (instance.Value.IsNullOrEmpty())
            {
                throw "The element '{0}' has no value (i.e. the value is either null or an empty string)"
                        .AsException(instance.Name.LocalName);
            }
            else if (TypeMapper.CannotResolveBetween<string, T>())
            {
                throw "No mapping exists between 'XElement (string)' and '{0}'. Use the overloaded ValueAs<T> to supply your own mapping method between 'XAttribute (string)' and '{0}'"
                        .AsException(typeof(T).Name);
            }
        }
    }

}
